-
Notifications
You must be signed in to change notification settings - Fork 17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Message passing with more information #291
Message passing with more information #291
Conversation
A lot of single quotes to double quotes changes came from my IDE linter, should be reduced if #289 get merged and this PR will contains much less line changes. |
ed2a043
to
96c5842
Compare
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #291 +/- ##
==========================================
+ Coverage 90.77% 90.90% +0.13%
==========================================
Files 22 22
Lines 2990 2999 +9
==========================================
+ Hits 2714 2726 +12
+ Misses 276 273 -3 ☔ View full report in Codecov by Sentry. |
43e1520
to
737adf3
Compare
737adf3
to
1b2864c
Compare
2ffdab2
to
d0e4e73
Compare
b218122
to
e3c2ae8
Compare
src/plumpy/base/state_machine.py
Outdated
@@ -300,16 +314,28 @@ def _fire_state_event(self, hook: Hashable, state: Optional[State]) -> None: | |||
def on_terminated(self) -> None: | |||
"""Called when a terminal state is entered""" | |||
|
|||
def transition_to(self, new_state: Union[Hashable, State, Type[State]], *args: Any, **kwargs: Any) -> None: | |||
def transition_to(self, new_state: State | type[State] | None, **kwargs: Any) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Build new_state with Hashable and from State class are the same, I made the use of transition_to more consistent by allow only passing the State class or an existed state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was wrong about this, using Hashable key to get the state type is more generic, it allows to redefine the state class dynamically for the specific state machine by overriding the STATES_MAP
. I'll revert changes back.
b1d9159
to
e5c74ad
Compare
7c105bd
to
17a541a
Compare
src/plumpy/processes.py
Outdated
state_class = self.get_states_map()[process_states.ProcessState.KILLED] | ||
new_state = self._create_state_instance(state_class, msg=msg) | ||
self.transition_to(new_state) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Compare to the one liner of original implementation, the new one seems more verbose. The state object needs to be created from the state tag with the parameters directly and passed to the transition_to
. But, in my opinion, this API is a bit readable. The transition_to
now only need to deal with the case where the passed in state is an object, instead of decide whether needs to create the state instance case by case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, to make this line more readable:
state_class = self.get_states_map()[process_states.ProcessState.KILLED]
may I suggest to somehow simplify it?
maybe this way:
@classmethod
def get_states_class(cls, state) -> Type[State]:
return cls.get_states_map()[state]
state_class = self.get_states_class(process_states.ProcessState.KILLED)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I did it by putting it into _create_state_instance
, and it gets better. THanks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @unkcpz, I did a first round, mainly asked some questions.
@@ -31,7 +45,7 @@ class StateEntryFailed(Exception): # noqa: N818 | |||
Failed to enter a state, can provide the next state to go to via this exception | |||
""" | |||
|
|||
def __init__(self, state: Hashable = None, *args: Any, **kwargs: Any) -> None: | |||
def __init__(self, state: State, *args: Any, **kwargs: Any) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is this change and what does it mean?
also please note the default value None
is taken away, is this backward compatible?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function inside plumpy is only called:
@super_check
def on_finish(self, result: Any, successful: bool) -> None:
"""Entering the FINISHED state."""
if successful:
validation_error = self.spec().outputs.validate(self.outputs)
if validation_error:
state_cls = self.get_states_map()[process_states.ProcessState.FINISHED]
finished_state = state_cls(self, result=result, successful=False)
raise StateEntryFailed(finished_state)
I did the same to create the state first and then pass to the function for consistency as transition_to
.
also please note the default value None is taken away, is this backward compatible?
It depends, I think we don't have a clear public API list for plumpy. Since aiida-core didn't use this directly, it does not break backward compatibility of aiida-core
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It depends, I think we don't have a clear public API list for plumpy. Since aiida-core didn't use this directly, it does not break backward compatibility of aiida-core.
ok, agreed, I'm also not so strict in this case, since so far aiida-core
is the only known dependent of this plumpy
src/plumpy/processes.py
Outdated
) -> None: | ||
# If we are creating, then reraise instead of failing. | ||
if final_state == process_states.ProcessState.CREATED: | ||
raise exception.with_traceback(trace) | ||
|
||
self.transition_to(process_states.ProcessState.EXCEPTED, exception, trace) | ||
state_class = self.get_states_map()[process_states.ProcessState.EXCEPTED] | ||
new_state = self._create_state_instance(state_class, exception=exception, trace_back=trace) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wherever self._create_state_instance
is called also self.get_states_map()
is called just before it.
Maybe it would make more sense to move that get_states_map()
logic directly inside _create_state_instance()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a good point, I did this for future factoring after and in the end I arrive at only accept the ProcessState
enum type as argument instead of accepting state object, you can have a look at https://github.com/unkcpz/plumpy/blob/6bfb87df69889f0082e8ecbe7732d24ad69e64c2/src/plumpy/base/state_machine.py#L159-L164
Is that more clear? I was plan to include change for a later release and am working on more refactoring for some interfaces.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, thank you. It makes sense to write a comment in the definition of _create_state_instance
, so to understand why you made this choice of design.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, I do the other way as you suggested, passing the process_states.ProcessState.EXCEPTED
directly to the _create_state_instance
and having self.get_states_map
call from inside.
src/plumpy/process_comms.py
Outdated
|
||
class KillMessage: | ||
@classmethod | ||
def build(cls, message: str | None = None, force: bool = False) -> MessageType: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd still call it force_kill
for clarity
def build(cls, message: str | None = None, force: bool = False) -> MessageType: | |
def build(cls, message: str | None = None, force_kill: bool = False) -> MessageType: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@unkcpz , maybe you forgot to update this 😅
src/plumpy/process_comms.py
Outdated
|
||
class KillMessage: | ||
@classmethod | ||
def build(cls, message: str | None = None, force: bool = False) -> MessageType: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
message = 'Killed through `verdi process kill`'
control.kill_processes(processes, all_entries=all_entries, timeout=timeout, wait=wait, message=message)
For this one, the message should be build first in to a KillMessage
and then send over.
message = KillMessage.build(message= 'Killed through `verdi process kill`')
control.kill_processes(processes, all_entries=all_entries, timeout=timeout, wait=wait, message=message)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the one in _perform_action
, it is the same, but I'd prefer to have the argument passing close to the function call of kill_process
_perform_actions(processes, controller.kill_process, 'kill', 'killing', timeout, wait, msg=message)
change it to
message = KillMessage.build(message=message, force=force)
_perform_actions(processes, functool.partial(controller.kill_process, msg=message), 'kill', 'killing', timeout, wait)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but now aiida-core
needs to import KillMessage
from plumpy
only for adding a flag. wouldn't be easier that msg
itself could be a dictionary?
message = {'msg' : 'Killed through `verdi process kill`',
'options' : {
'force_kill':True
}}
control.kill_processes(processes, controller.kill_process, .., msg=message)
and then plumpy
would well receive all options listed, if any.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but now aiida-core needs to import KillMessage from plumpy only for adding a flag
Why it is a problem for importing KillMessage
? I think message is a good interface that can flexibly support backward compatibility. We make all four message type public APIs of plumpy and when using process communicator it requires sending over through this message. In the future even if we want to change the real message to other type we can do it without break users' code.
wouldn't be easier that msg itself could be a dictionary?
It is a dictionary returned by build
but construct a dictionary by hand is error-prone. Using the build
function of the message, the function signature will tell developers which options are allowed to be passed. We can make this works also by dataclass, but then that is overkill for a message in my opinion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
well this is the twisted things that we were always complaining about, import from plumpy to only make a dictionary only for an input of a plumpy function itself...
I mean you see my point 😄 🤌
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My 2¢'s: I think it is rarely preferable to have a free-form dictionary to specify inputs to an API, especially if it is nested, because you will always have to start reading the docs/code to know how it is structured. Using the other approach, (data) classes, explicit function/method arguments etc. these can be auto inspected by an IDE and are much easier to use. The fact that you have to import something is in my eyes not really a problem. Anyway to use a library you have to import something to do anything.
Now in this case, ideally the force_kill
argument would have been directly available alongside the message
to whatever method/function is called. But if this is not possible with the current design and it has to be incorporated in the message itself, than the KillMessage
approach would be a good alternative. I would perhaps just consider making it a proper dataclass to get automatic type validation, e.g.
from dataclass import dataclass
@dataclass
class KillMessage:
message: str | None = None
force_kill: bool = False
@classmethod
def build(...):
....
Actually, if using a dataclass, why not just use the constructor to build the instance instead of using the build
classmethod. KillMessage(message="something, force_kill=True)
is nicer than KillMessage.build(message="something, force_kill=True)
I think
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
import from plumpy to only make a dictionary only for an input of a plumpy function itself
I won't say "only", encapsulate it in a function as a contract also serve as an input validator.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry @sphuber, I just saw your comment, I had my browser open for during the weekend and I directly reply to Ali's comment without refresh.
Yes, thought about dataclass, and I think it can work. I use build
into a dict mostly for the reason to keep the message the same as what was passing as before, since this is the message that should also go through de/serialize by the en/decoder and I am not 100% confident with that.
I think msgpack will take care of the dataclass for sure, but since the en/decoder used was the yaml one, I am not so sure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same here 😄 I had the page open without refreshing, so I didn't notice Sebastiaan's comment.
KillMessage(message="something", force_kill=True) is nicer than KillMessage.build(message="something", force_kill=True) I think
I see and agree with this suggestion, ✔️
src/plumpy/process_comms.py
Outdated
|
||
class KillMessage: | ||
@classmethod | ||
def build(cls, message: str | None = None, force: bool = False) -> MessageType: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My 2¢'s: I think it is rarely preferable to have a free-form dictionary to specify inputs to an API, especially if it is nested, because you will always have to start reading the docs/code to know how it is structured. Using the other approach, (data) classes, explicit function/method arguments etc. these can be auto inspected by an IDE and are much easier to use. The fact that you have to import something is in my eyes not really a problem. Anyway to use a library you have to import something to do anything.
Now in this case, ideally the force_kill
argument would have been directly available alongside the message
to whatever method/function is called. But if this is not possible with the current design and it has to be incorporated in the message itself, than the KillMessage
approach would be a good alternative. I would perhaps just consider making it a proper dataclass to get automatic type validation, e.g.
from dataclass import dataclass
@dataclass
class KillMessage:
message: str | None = None
force_kill: bool = False
@classmethod
def build(...):
....
Actually, if using a dataclass, why not just use the constructor to build the instance instead of using the build
classmethod. KillMessage(message="something, force_kill=True)
is nicer than KillMessage.build(message="something, force_kill=True)
I think
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot @unkcpz, I added more minor comments. I think overall looks good, I think we should really consider the suggestion by Sebastiaan
KillMessage(message="something", force_kill=True)
I think that would simplify the api and also makes it more readable & usable in aiida-core
, in general.
@@ -31,7 +45,7 @@ class StateEntryFailed(Exception): # noqa: N818 | |||
Failed to enter a state, can provide the next state to go to via this exception | |||
""" | |||
|
|||
def __init__(self, state: Hashable = None, *args: Any, **kwargs: Any) -> None: | |||
def __init__(self, state: State, *args: Any, **kwargs: Any) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It depends, I think we don't have a clear public API list for plumpy. Since aiida-core didn't use this directly, it does not break backward compatibility of aiida-core.
ok, agreed, I'm also not so strict in this case, since so far aiida-core
is the only known dependent of this plumpy
src/plumpy/process_comms.py
Outdated
|
||
class KillMessage: | ||
@classmethod | ||
def build(cls, message: str | None = None, force: bool = False) -> MessageType: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same here 😄 I had the page open without refreshing, so I didn't notice Sebastiaan's comment.
KillMessage(message="something", force_kill=True) is nicer than KillMessage.build(message="something", force_kill=True) I think
I see and agree with this suggestion, ✔️
src/plumpy/processes.py
Outdated
) -> None: | ||
# If we are creating, then reraise instead of failing. | ||
if final_state == process_states.ProcessState.CREATED: | ||
raise exception.with_traceback(trace) | ||
|
||
self.transition_to(process_states.ProcessState.EXCEPTED, exception, trace) | ||
state_class = self.get_states_map()[process_states.ProcessState.EXCEPTED] | ||
new_state = self._create_state_instance(state_class, exception=exception, trace_back=trace) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, thank you. It makes sense to write a comment in the definition of _create_state_instance
, so to understand why you made this choice of design.
src/plumpy/process_comms.py
Outdated
|
||
class KillMessage: | ||
@classmethod | ||
def build(cls, message: str | None = None, force: bool = False) -> MessageType: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@unkcpz , maybe you forgot to update this 😅
src/plumpy/processes.py
Outdated
state_class = self.get_states_map()[process_states.ProcessState.KILLED] | ||
new_state = self._create_state_instance(state_class, msg=msg) | ||
self.transition_to(new_state) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, to make this line more readable:
state_class = self.get_states_map()[process_states.ProcessState.KILLED]
may I suggest to somehow simplify it?
maybe this way:
@classmethod
def get_states_class(cls, state) -> Type[State]:
return cls.get_states_map()[state]
state_class = self.get_states_class(process_states.ProcessState.KILLED)
According to chatgpt, if the dataclass used with yaml loader for serialize/deserialize, it need to be passed as dict. I think |
Discussed with @khsrali, I think we agree on the API which seems more user friendly for construct different messages. By having a collection type
|
b6cd8b3
to
ba7c821
Compare
bcd64bc
to
d725a51
Compare
Thanks a lot @unkcpz, looks good! msg = MessageBuilder.kill(text='killed through `verdi process kill` ', force_kill = True)
_perform_actions(processes, controller.kill_process, 'kill', 'killing', timeout, wait, msg=message) Can you please open a matching PR in |
Sure! I think we can merge this PR and continue with #288. Then we make a 0.23.0 release. I'll then open a PR and tested in aiida-core? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @khsrali. Yes, I'll wait till tomorrow if Seb has any more comments to add. |
…on (aiidateam#291) The messages to passing over rabbitmq for process control is build dynamically and able to carry more information. In the old implementation, the messages ace global dictionary variables and when the message need to change by copy which is error-prone. This commit introduce the `MessageBuilder` with class methods for creating kill/pause/status/play messages. For "kill" message, I also add support for passing the `force_kill` option.
_create_state_instance
so it only need to do real object creation.